11 Thread
Gli oggetti forniscono un modo per strutturare il programma in sezioni indipendenti. Spesso, però, è necessario dividere il programma, in parti eseguibili indipendentemente e contemporaneamente, chiamate thread.
Un thread è un singolo flusso di controllo sequenziale in un programma.
Il modo più semplice per creare un thread è quello di derivare la classe Thread che ha tutti i meccanismi necessari per creare ed eseguire i thread.
Il metodo più importante della classe Thread è run() che rappresenta il punto di entrata per il thread.
Oltre al metodo run() la classe Thread definisce altri metodi per la gestione dei Thread:
getName ottiene il nome di un thread
getPriority ottiene la priorità di un thread
isAlive determina se un thread è ancora in esecuzione
sleep sospende un thread per un determinato numero di millisecondi
start avvia un thread chiamando il relativo metodo di esecuzione
suspend sospende un thread (deprecato perchè è “deadlock-prone”)
Quando un programma Java viene avviato, è già in esecuzione un thread che viene solitamente denominato main thread, oltre che il thread del garbage collector ed altri thread che gestisce direttamente l’interprete.
Il main thread è importante per due motivi:
è il thread da cui saranno generati gli altri thread
deve essere l’ultimo thread a terminare
Se si vuole un thread con un ciclo di vita indipendente dal main thread si realizza un cosiddetto thread daemon col metodo setDaemon().
E’ possibile controllare il thread principale, mediante un oggetto Thread, che è creato automaticamente all’avvio del programma. A tale scopo viene fornito un metodo statico pubblico della classe Thread denominato currentThread(), permettendo di gestirlo come un qualsiasi altro thread.
12 Creazione di un Thread
In generale un thread può essere creato generando una istanza della classe Thread. E’ possibile fare ciò in due modi:
Implementando l’interfaccia Runnable
Estendendo la classe Thread
12.1 Estensione della classe Thread
public class SimpleThread extends Thread {
private int countDown = 5;
private static int threadCount = 0;
private int threadNumber = ++threadCount;
public SimpleThread() {
System.out.println("Making " + threadNumber);
}
// Va implementato il metodo public void run(), altrimenti di default questo non fa niente
public void run() {
while (true) {
System.out.println("Thread " + threadNumber + "(" + countDown + ")");
if(--countDown == 0) return;
}
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new SimpleThread().start(); // Se chiamassi run() l'esecuzione partirebbe ma nello stesso thread. Nell'implementazione di start() c'è il modo per comunicare al sistema operativo la partenza del nuovo thread e, successivamente, chiama run()
}
System.out.println("All Threads Started");
}
}
12.2 Interfaccia Runnable
Per implementare l’interfaccia Runnable, una classe deve implementare il metodo run().
Il metodo run() è un metodo come tutti gli altri, con la differenza che stabilisce il punto di entrata per un nuovo thread all’interno del programma.
public class SimpleThreadWithRunnable implements Runnable {
private int countDown = 5;
private static int threadCount = 0;
private int threadNumber = ++threadCount;
private Thread th; // attributo di tipo Thread (composizione)
SimpleThreadWithRunnable(String name) {
= new Thread(this, name); // Devo richiamare io il costruttore di Thread, devo usare il costruttore dove passo l'istanza dell'oggetto, o in alternativa posso passare anche una String come parametro per dargli un nome
th System.out.println("Making " + threadNumber);
}
public void run() { // Qui non sto sovrascrivendo run() di Thread ma sto implementando l'operazione di Runnable
while (true) {
System.out.println("Thread " + threadNumber + "(" + countDown + ")");
if(--countDown == 0) return;
}
}
public void start_thread() { // metodo per far partire il thread
.start(); // fa partire il thread
th}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new SimpleThreadWithRunnable("").start_thread;
}
System.out.println("All Threads Started");
}
}
12.3 Multithreading
// Crea più thread
class MyThread implements Runnable {
String name; //nome del thread
Thread th;
MyThread(String thread_name) {
= thread_name;
name = new Thread(this, name);
th System.out.println("New thread: " + th);
.start(); //fa partire il thread
th}
// entry point
public void run() {
try {
for (int i = 3; i > 0; i--) {
System.out.println(name + ": " + i);
Thread.sleep(1500);
}
} catch (InterruptedException e) { // sleep può espellere un'eccezione
System.out.println(name + ": interruzione");
}
System.out.println(name + ": uscita");
}
}
public class MyMultiThread {
public static void main(String args[]) {
new MyThread("Uno");
new MyThread("Due");
new MyThread("Tre");
}
}
12.4 Condivisione di risorse
Con il multithreading si può verificare la possibilità che due o più thread cercano di accedere ad una risorsa limitata condivisa. In tal caso è necessario prevedere dei metodi che siano in grado di garantire l’utilizzo esclusivo della risorsa da parte di un thread per volta.
Il processo appena esposto viene chiamato sincronizzazione.
Java fornisce lo specificatore synchronized per quanto riguarda i metodi. Solo un thread alla volta può chiamare un metodo synchronized per l’oggetto sul quale è invocato (sebbene un thread possa chiamare più metodi sincronizzati per l’oggetto).
public class SyncThread {
private TwoCounter t = new TwoCounter();
SyncThread() {
.start(); // faccio partire il thread
tnew Watcher();
}
public static void main(String args[]) {
= new SyncThread();
SyncThread s }
class TwoCounter extends Thread {
private int a = 0;
private int b = 0;
public void run() {
while (true) {
synchronized (this) {
++;
a++;
b}
}
}
// synchTest è eseguito solo quando run non è in esecuzione nello stesso Thread e viceversa
public synchronized void synchTest() {
System.out.println("a" + a + "b" + b);
}
}
class Watcher extends Thread {
public Watcher() {
start();
}
public void run() {
while (true) {
.synchTest();
t}
}
}
}
Nell’esempio il thread della classe SyncThread tramite il metodo run() incrementa due contatori. In più c’è un altro thread della classe Watcher che stampa il contenuti dei due contatori.
Senza la parola chiave synchronized potrebbe accadere che di tanto in tanto i due contatori diventano diversi. Ma aggiungendo la parola synchronized a run() il Watcher non riesce mai a chiamare synchTest fino a quando la risorsa non è sbloccata, ma questo non avviene mai, quindi avremmo un deadlock. È possibile invece limitare la sincronizzazione alla sola sezione critica relativa ad un oggetto. L’esempio precedente quindi utilizza synchronized nelle linee critiche, cioè il while(true), sull’oggetto corrente(this).
Utilizziamo quindi synchronized per fare in modo che più istruzioni vengano svolte sempre di fila, come se fossero una transazione unica.
L’uso di synchronized è molto svantaggioso dal punto dell’efficienza, infatti si potrebbero verificare dei bottleneck.
D’altra parte una condivisione non corretta delle risorse potrebbe portare a eventi disastrosi per il programma.
Si conclude che la sincronizzazione deve essere effettuata solo quando è necessaria.
12.5 Diagramma di stato di un thread
Un thread può trovarsi in uno dei seguenti stati: - New: l’oggetto thread è stato creato ma non è stato ancora avviato quindi non può essere in esecuzione. Lo scheduler non sa ancora della sua esistenza.
Runnable: in questa fase un thread può essere in esecuzione quando il meccanismo di time-slicing ha dei cicli di CPU accessibili ai thread. Di conseguenza un thread potrebbe o meno essere in esecuzione e nulla gli impedisce di essere eseguito se richiesto dallo scheduler. Con yield() restituiamo volontariamente il time-slice al sistema operativo.
Dead: il classico modo per terminare un thread è quello di uscire dal metodo run(). Tuttavia è possibile anche farlo mediante l’invocazione dei metodi stop() o destroy(), ma questi sono stati deprecati in quanto il thread potrebbe lasciare il sistema in uno stato inconsistente. È preferibile forzare l’arresto di un thread attraverso un flag che permetta di uscire al metodo run().
Blocked: il thread potrebbe essere in esecuzione ma qualcosa lo impedisce. Quando un thread è bloccato lo scheduler lo salterà evitando di concedergli il time-slice. Finché il thread non ritorna nello stato Runnable non effettuerà alcuna operazione.
Un thread può portarsi nello stato blocked per le seguenti ragioni:
Si è volontariamente sospeso il thread mediante l’invocazione del metodo sleep
È stata sospesa l’esecuzione del thread con il metodo suspend(). Il thread non tornerà nello stato Runnable finché non si ha un messaggio di resume().
È stata sospesa l’esecuzione del thread con il metodo wait() pertanto non tornerà nello stato Runnable finchè non si ha un messaggio di notify() oppure notifyAll()
Il thread sta aspettando che sia completato un’operazione di I/O.
Il thread sta cercando di chiamare un metodo sincronizzato oppure un altro oggetto non accessibile.
12.6 Priorità
La priorità di un thread fornisce allo scheduler informazioni sull’ordine di esecuzione.
Nel caso ci sono un certo numero di thread bloccati, in attesa di esecuzioni, lo scheduler lancerà quello con priorità più alta. Per impostare la priorità di thread si deve utilizzare il metodo setPriority() della classe Thread.
La sintassi generale è la seguente:
final void setPriority(int livello);
Il livello specifica la nuova impostazione per il thread chiamante. Il valore assunto deve variare nell’intervallo MIN_PRIORITY ( = 1) e MAX_PRIORITY ( = 10). Per ripristinare il valore di default, si deve specificare NORM_PRIORITY ( = 5).
Per ottenere l’impostazione attuale della priorità, occorre chiamare il metodo getPriority() di Thread nel modo seguente: ```Java final int getPriority()